// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of 'program_builder.dart';

/// Generates the code for all used classes in the program. Static fields (even
/// in classes) are ignored, since they can be treated as non-class elements.
///
/// The code for the containing (used) methods must exist in the `universe`.
class Collector {
  final JCommonElements _commonElements;
  final JElementEnvironment _elementEnvironment;
  final OutputUnitData _outputUnitData;
  final CodegenWorld _codegenWorld;
  final Emitter _emitter;
  final NativeData _nativeData;
  final InterceptorData _interceptorData;
  final OneShotInterceptorData _oneShotInterceptorData;
  final JClosedWorld _closedWorld;
  final Iterable<ClassEntity> _rtiNeededClasses;
  final Map<MemberEntity, js.Expression> _generatedCode;
  final Sorter _sorter;

  final Set<ClassEntity> neededClasses = {};
  final Set<ClassEntity> neededClassTypes = {};
  final Set<ClassEntity> classesOnlyNeededForConstructor = {};
  final Map<OutputUnit, List<ClassEntity>> outputClassLists = {};
  final Map<OutputUnit, List<ClassEntity>> outputClassTypeLists = {};
  final Map<OutputUnit, List<ConstantValue>> outputConstantLists = {};
  final Map<OutputUnit, List<MemberEntity>> outputStaticLists = {};
  final Map<OutputUnit, List<FieldEntity>> outputStaticNonFinalFieldLists = {};
  final Map<OutputUnit, List<FieldEntity>> outputLazyStaticFieldLists = {};
  final Map<OutputUnit, Set<LibraryEntity>> outputLibraryLists = {};

  /// True, if the output contains a constant list.
  ///
  /// This flag is updated in [computeNeededConstants].
  bool outputContainsConstantList = false;

  final List<ClassEntity> nativeClassesAndSubclasses = [];

  Collector(
    this._commonElements,
    this._elementEnvironment,
    this._outputUnitData,
    this._codegenWorld,
    this._emitter,
    this._nativeData,
    this._interceptorData,
    this._oneShotInterceptorData,
    this._closedWorld,
    this._rtiNeededClasses,
    this._generatedCode,
    this._sorter,
  );

  Set<ClassEntity> computeInterceptorsReferencedFromConstants() {
    Set<ClassEntity> classes = {};
    Iterable<ConstantValue> constants = _codegenWorld.getConstantsForEmission();
    for (ConstantValue constant in constants) {
      if (constant is InterceptorConstantValue) {
        InterceptorConstantValue interceptorConstant = constant;
        classes.add(interceptorConstant.cls);
      }
    }
    return classes;
  }

  /// Return a function that returns true if its argument is a class
  /// that needs to be emitted.
  bool Function(ClassEntity cls) computeClassFilter(
    Iterable<ClassEntity> backendTypeHelpers,
  ) {
    Set<ClassEntity> unneededClasses = {};
    // The [Bool] class is not marked as abstract, but has a factory
    // constructor that always throws. We never need to emit it.
    unneededClasses.add(_commonElements.boolClass);

    // Go over specialized interceptors and then constants to know which
    // interceptors are needed.
    Set<ClassEntity> needed = {};
    for (SpecializedGetInterceptor interceptor
        in _oneShotInterceptorData.specializedGetInterceptors) {
      needed.addAll(interceptor.classes);
    }

    // Add interceptors referenced by constants.
    needed.addAll(computeInterceptorsReferencedFromConstants());

    // Add unneeded interceptors to the [unneededClasses] set.
    for (ClassEntity interceptor in _interceptorData.interceptedClasses) {
      if (!needed.contains(interceptor) &&
          interceptor != _commonElements.objectClass) {
        unneededClasses.add(interceptor);
      }
    }

    // These classes are just helpers for the backend's type system.
    unneededClasses.addAll(backendTypeHelpers);

    return (ClassEntity cls) => !unneededClasses.contains(cls);
  }

  // Return the classes that are just helpers for the backend's type system.
  static Iterable<ClassEntity> getBackendTypeHelpers(
    JCommonElements commonElements,
  ) {
    return [
      commonElements.jsMutableArrayClass,
      commonElements.jsFixedArrayClass,
      commonElements.jsExtendableArrayClass,
      // TODO(johnniwinther): Mark this as a backend type helper:
      //commonElements.jsUnmodifiableArrayClass,
      commonElements.jsUInt32Class,
      commonElements.jsUInt31Class,
      commonElements.jsPositiveIntClass,
    ];
  }

  /// Compute all the constants that must be emitted.
  void computeNeededConstants() {
    Iterable<ConstantValue> constants = _codegenWorld.getConstantsForEmission(
      _emitter.compareConstants,
    );
    for (ConstantValue constant in constants) {
      if (_emitter.isConstantInlinedOrAlreadyEmitted(constant)) continue;

      if (constant is ListConstantValue) outputContainsConstantList = true;

      // The back-end introduces some constants, like "InterceptorConstant" or
      // some list constants. They are emitted in the main output-unit.
      // TODO(sigurdm): We should track those constants.
      final constantUnit = _outputUnitData.outputUnitForConstant(constant);
      outputConstantLists.putIfAbsent(constantUnit, () => []).add(constant);
    }
  }

  /// Compute all the classes and typedefs that must be emitted.
  void computeNeededDeclarations() {
    Set<ClassEntity> backendTypeHelpers =
        getBackendTypeHelpers(_commonElements).toSet();

    // Compute needed classes.
    Set<ClassEntity> instantiatedClasses =
        // TODO(johnniwinther): This should be accessed from a codegen closed
        // world.
        _codegenWorld.directlyInstantiatedClasses
            .where(computeClassFilter(backendTypeHelpers))
            .toSet();

    void addClassesWithSuperclasses(Iterable<ClassEntity> classes) {
      for (ClassEntity cls in classes) {
        neededClasses.add(cls);
        _elementEnvironment.forEachSuperClass(
          cls,
          (superClass) => neededClasses.add(superClass),
        );
      }
    }

    // 1. We need to generate all classes that are instantiated.
    addClassesWithSuperclasses(instantiatedClasses);

    // 2. Add all classes used as mixins.
    Set<ClassEntity> mixinClasses =
        neededClasses
            .map(_elementEnvironment.getEffectiveMixinClass)
            .whereType<ClassEntity>()
            .toSet();
    neededClasses.addAll(mixinClasses);

    // 3. Add classes only needed for their constructors.
    for (var cls in _codegenWorld.constructorReferences) {
      if (neededClasses.add(cls)) {
        classesOnlyNeededForConstructor.add(cls);
      }
    }

    // 4. Find all class types needed for rti.
    for (ClassEntity cls in _rtiNeededClasses) {
      if (backendTypeHelpers.contains(cls)) continue;
      neededClassTypes.add(cls);
    }

    // 5. Sort classes and add them to their respective OutputUnits.
    for (ClassEntity cls in _sorter.sortClasses(neededClasses)) {
      if (_nativeData.isNativeOrExtendsNative(cls) &&
          !classesOnlyNeededForConstructor.contains(cls)) {
        // For now, native classes and related classes cannot be deferred.
        nativeClassesAndSubclasses.add(cls);
        assert(!_outputUnitData.isDeferredClass(cls), failedAt(cls));
        outputClassLists
            .putIfAbsent(_outputUnitData.mainOutputUnit, () => [])
            .add(cls);
      } else {
        outputClassLists
            .putIfAbsent(_outputUnitData.outputUnitForClass(cls), () => [])
            .add(cls);
      }
    }

    // 6. Sort classes needed for type checking and then add them to their
    // respective OutputUnits.
    for (ClassEntity cls in _sorter.sortClasses(neededClassTypes)) {
      outputClassTypeLists
          .putIfAbsent(_outputUnitData.outputUnitForClassType(cls), () => [])
          .add(cls);
    }
  }

  void computeNeededStatics() {
    bool isStaticFunction(MemberEntity element) =>
        !element.isInstanceMember && element is! FieldEntity;

    Iterable<MemberEntity> elements = _generatedCode.keys.where(
      isStaticFunction,
    );

    for (MemberEntity member in _sorter.sortMembers(elements)) {
      List<MemberEntity> list = outputStaticLists.putIfAbsent(
        _outputUnitData.outputUnitForMember(member),
        () => [],
      );
      list.add(member);
    }
  }

  void computeNeededStaticNonFinalFields() {
    void addToOutputUnit(FieldEntity element) {
      List<FieldEntity> list = outputStaticNonFinalFieldLists.putIfAbsent(
        _outputUnitData.outputUnitForMember(element),
        () => [],
      );
      list.add(element);
    }

    List<FieldEntity> eagerFields = [];
    _codegenWorld.forEachStaticField((FieldEntity field) {
      if (_closedWorld.fieldAnalysis.getFieldData(field as JField).isEager) {
        eagerFields.add(field);
      }
    });

    eagerFields.sort((FieldEntity a, FieldEntity b) {
      FieldAnalysisData aFieldData = _closedWorld.fieldAnalysis.getFieldData(
        a as JField,
      );
      FieldAnalysisData bFieldData = _closedWorld.fieldAnalysis.getFieldData(
        b as JField,
      );
      final aIndex = aFieldData.eagerCreationIndex;
      final bIndex = bFieldData.eagerCreationIndex;
      if (aIndex != null && bIndex != null) {
        return aIndex.compareTo(bIndex);
      } else if (aIndex != null) {
        // Sort [b] before [a].
        return 1;
      } else if (bIndex != null) {
        // Sort [a] before [b].
        return -1;
      } else {
        return _sorter.compareMembersByLocation(a, b);
      }
    });
    eagerFields.forEach(addToOutputUnit);
  }

  void computeNeededLazyStaticFields() {
    List<FieldEntity> lazyFields = [];
    _codegenWorld.forEachStaticField((FieldEntity field) {
      if (_closedWorld.fieldAnalysis.getFieldData(field as JField).isLazy) {
        lazyFields.add(field);
      }
    });

    for (FieldEntity field in _sorter.sortMembers(lazyFields)) {
      OutputUnit unit = _outputUnitData.outputUnitForMember(field);
      (outputLazyStaticFieldLists[unit] ??= []).add(field);
    }
  }

  void computeNeededLibraries() {
    for (var element in _generatedCode.keys) {
      OutputUnit unit = _outputUnitData.outputUnitForMember(element);
      LibraryEntity library = element.library;
      outputLibraryLists.putIfAbsent(unit, () => {}).add(library);
    }
    for (var element in neededClasses) {
      OutputUnit unit = _outputUnitData.outputUnitForClass(element);
      LibraryEntity library = element.library;
      outputLibraryLists.putIfAbsent(unit, () => {}).add(library);
    }
    for (var element in neededClassTypes) {
      OutputUnit unit = _outputUnitData.outputUnitForClassType(element);
      LibraryEntity library = element.library;
      outputLibraryLists.putIfAbsent(unit, () => {}).add(library);
    }
  }

  void collect() {
    computeNeededDeclarations();
    computeNeededConstants();
    computeNeededStatics();
    computeNeededStaticNonFinalFields();
    computeNeededLazyStaticFields();
    computeNeededLibraries();
  }
}
